package jamirr.apt;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.Filer;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Target;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public final class ProcessorContext {

    private final Elements elementUtils;
    private final Types typeUtils;
    private final Filer filer;

    private final Set<String> mirrorAnnotations = new HashSet<>();

    public ProcessorContext(Elements elementUtils, Types typeUtils, Filer filer) {
        this.elementUtils = elementUtils;
        this.typeUtils = typeUtils;
        this.filer = filer;
    }

    public void addMirrorAnnotation(String annotation) {
        mirrorAnnotations.add(annotation);
    }

    public boolean isMirrorAnnotation(String annotation) {
        return !isInternalAnnotation(annotation)
                && mirrorAnnotations.contains(annotation);
    }

    public boolean isMirrorAnnotation(AnnotationMirror annotation) {
        return mirrorAnnotations.contains(annotation.getAnnotationType().toString());
    }

    public boolean isInternalAnnotation(String annotation) {
        return annotation.startsWith("java.lang.annotation.");
    }

    public boolean isInternalAnnotation(AnnotationMirror annotation) {
        return isInternalAnnotation(annotation.getAnnotationType().toString());
    }

    public boolean isInternalAnnotation(DeclaredType declaredType) {
        return isInternalAnnotation(declaredType.asElement().toString());
    }

    public boolean isInheritedAnnotated(AnnotationMirror annotation) {
        Inherited inherited = annotation.getAnnotationType()
                .asElement()
                .getAnnotation(Inherited.class);
        return inherited != null;
    }

    public boolean isTypeElementAnnotated(DeclaredType declaredType) {
        Target target = declaredType.asElement().getAnnotation(Target.class);
        return target != null && Arrays.asList(target.value()).contains(ElementType.TYPE);
    }

    public boolean isAnnotationTypeElementAnnotated(DeclaredType declaredType) {
        Target target = declaredType.asElement().getAnnotation(Target.class);
        return target != null && Arrays.asList(target.value()).contains(ElementType.ANNOTATION_TYPE);
    }

    public boolean isSame(TypeMirror type, String className) {
        TypeElement typeOfClazzElement = elementUtils.getTypeElement(className);
        if (type.getKind() == TypeKind.DECLARED) {
            DeclaredType declaredType = (DeclaredType) type;
            if (declaredType.getTypeArguments().size() != typeOfClazzElement.getTypeParameters().size()) {
                return false;
            }
            TypeMirror[] typeArgs = declaredType.getTypeArguments().toArray(new TypeMirror[0]);
            DeclaredType typeOfClazz = typeUtils.getDeclaredType(typeOfClazzElement, typeArgs);
            return typeUtils.isSameType(type, typeOfClazz);
        }
        return false;
    }

    public String getPackageNameOf(DeclaredType type) {
        return getPackageNameOf(type.asElement());
    }

    public String getPackageNameOf(Element element) {
        return elementUtils.getPackageOf(element).getQualifiedName().toString();
    }

    public String getSimpleName(DeclaredType type) {
        return getSimpleName(type.asElement());
    }

    public String getSimpleName(Element element) {
        return element.getSimpleName().toString();
    }

    public String getFullyQualifiedName(DeclaredType type) {
        return getFullyQualifiedName(type.asElement());
    }

    public String getFullyQualifiedName(Element element) {
        return ((TypeElement) element).getQualifiedName().toString();
    }

    public ClassName getClassName(String canonicalName) {
        TypeElement element = elementUtils.getTypeElement(canonicalName);
        return ClassName.get(getPackageNameOf(element), getSimpleName(element));
    }

    public ClassName getClassName(TypeMirror typeMirror) {
        Element element = typeUtils.asElement(typeMirror);
        return ClassName.get(getPackageNameOf(element), getSimpleName(element));
    }

    public ClassName getClassName(Class<?> type) {
        String canonicalName = type.getCanonicalName();
        return getClassName(canonicalName);
    }

    public <T> T getAnnotationValue(AnnotationMirror mirror, String methodName, Class<T> clazz) {
        AnnotationValue annotationValue = getAnnotationValue(mirror, methodName);
        return annotationValue == null ? null : clazz.cast(annotationValue.getValue());
    }

    public AnnotationValue getAnnotationValue(AnnotationMirror mirror, String methodName) {
        ExecutableElement method = mirror.getAnnotationType()
                .asElement()
                .getEnclosedElements()
                .stream()
                .filter(e -> isMethodWithName(e, methodName))
                .map(ExecutableElement.class::cast)
                .findAny()
                .orElse(null);

        return mirror.getElementValues().get(method);
    }

    private boolean isMethodWithName(Element e, String methodName) {
        return ElementKind.METHOD == e.getKind() && methodName.equals(e.getSimpleName().toString());
    }

    public List<ExecutableElement> methodsIn(DeclaredType declaredType) {
        return ElementFilter.methodsIn(declaredType.asElement().getEnclosedElements());
    }

    public void writeJavaFile(String packageName, TypeSpec typeSpec) {
        var javaFile = JavaFile.builder(packageName, typeSpec)
                .build();
        try {
            javaFile.writeTo(filer);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void writeMetaInf(Class<?> serviceType, String serviceImplClassName) {
        // 生成配置文件
        String resourceFile = "META-INF/jamirr/" + serviceType.getCanonicalName() + "/" + serviceImplClassName;
        try {
            FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resourceFile);
            try (OutputStream out = resource.openOutputStream()) {
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
                writer.write("// Generated by Jamirr Annotation Processor. Do not delete this file.");
                writer.flush();
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
